package com.sk.compiler.processor;


import com.google.auto.service.AutoService;
import com.sk.api.annotations.Bean;
import com.sk.api.annotations.Component;
import com.sk.compiler.info.NameInfo;
import com.sk.compiler.info.build.BuilderInfo;
import com.sk.compiler.info.build.BuilderUtils;
import com.sk.compiler.info.build.MethodInfo;
import com.sk.compiler.info.build.ParamInfo;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.sk.api.annotations.Bean","com.sk.api.annotations.Component"})
public class FactoryProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, BuilderInfo> map=loadInfo(annotations,roundEnv);
//        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,map.toString());
        writeFile(map);
        return true;
    }

    private void writeFile(Map<String, BuilderInfo> map) {
        for (String key:map.keySet()){
            try {
                writeFile(key,map.get(key));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void writeFile(String key, BuilderInfo builderInfo) throws IOException {
        if(builderInfo.isComponent()){
            writeSelf(key,builderInfo);
        }
        List<MethodInfo> methodInfos = builderInfo.getMethodInfos();
        for (MethodInfo methodInfo : methodInfos){
            writeMethod(key, methodInfo);
        }
        NameInfo nameInfo=new NameInfo(key,"$Builder");
        JavaFileObject sourceFile = processingEnv.getFiler()
                .createSourceFile(nameInfo.getNewFullName());
        Writer writer = sourceFile.openWriter();
        String content= BuilderUtils.buildBuilder(nameInfo,builderInfo);
        writer.write(content);
        writer.close();
    }

    private void writeMethod(String className, MethodInfo methodInfo) throws IOException {
        NameInfo nameInfo=new NameInfo(className,"$"+ methodInfo.getMethodName()+"$Factory");
        JavaFileObject sourceFile = processingEnv.getFiler()
                .createSourceFile(nameInfo.getNewFullName());
        Writer writer = sourceFile.openWriter();
        String content=BuilderUtils.buildBeanFactory(nameInfo, methodInfo);
        writer.write(content);
        writer.close();
    }

    private void writeSelf(String key, BuilderInfo builderInfo) throws IOException {
        NameInfo nameInfo=new NameInfo(key,"$Factory");
        JavaFileObject sourceFile = processingEnv.getFiler()
                .createSourceFile(nameInfo.getNewFullName());
        Writer writer = sourceFile.openWriter();
        String content= BuilderUtils.buildComponentFactory(nameInfo,builderInfo);
        writer.write(content);
        writer.close();
    }

    private Map<String, BuilderInfo> loadInfo(Set<? extends TypeElement> annotations,
                                              RoundEnvironment roundEnv) {
        Map<String, BuilderInfo> map=new HashMap<>();
        for (TypeElement annotation:annotations){
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
            for(Element element:elements){
                if(element.getAnnotation(Component.class)!=null){
                    handleComponent((TypeElement) element,map);
                }else {
                    handleBean((ExecutableElement) element,map);
                }
            }
        }
        return map;
    }

    private void handleBean(ExecutableElement element, Map<String, BuilderInfo> map) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        BuilderInfo builderInfo =getOrCreate(map,typeElement.getQualifiedName().toString());
        MethodInfo methodInfo =new MethodInfo();
        Bean bean = element.getAnnotation(Bean.class);
        methodInfo.setSingleton(bean.singleton());
        String name= bean.value();
        String methodName=element.getSimpleName().toString();
        methodInfo.setMethodName(methodName);
        if(name.equals(""))name=methodName;
        methodInfo.setName(name);
        methodInfo.setClassName(element.getReturnType().toString());
        List<? extends VariableElement> parameters = element.getParameters();
        for (VariableElement parameter:parameters){
            methodInfo.addParamInfo(new ParamInfo(parameter.getSimpleName().toString(),parameter.asType().toString()));
        }
        builderInfo.addMethodInfo(methodInfo);
    }

    private void handleComponent(TypeElement element, Map<String, BuilderInfo> map) {
        String className = element.getQualifiedName().toString();
        BuilderInfo builderInfo =getOrCreate(map, className);
        builderInfo.setComponent(true);
        Component component = element.getAnnotation(Component.class);
        builderInfo.setSingleton(component.singleton());
        String name= component.value();
        if(name.equals(""))name=getName(className);
        builderInfo.setName(name);
    }

    private String getName(String className) {
        String name = className.substring(className.lastIndexOf('.') + 1);
        return name.substring(0,1).toLowerCase()+name.substring(1);
    }

    private BuilderInfo getOrCreate(Map<String, BuilderInfo> map, String className) {
        if(!map.containsKey(className)){
            map.put(className,new BuilderInfo());
        }
        return map.get(className);
    }

}